package dna;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.regex.Pattern;

import core.inputoutput.formatting.Format;
import core.inputoutput.formatting.FormattedEntry;
import core.inputoutput.parsers.PDBParser;
import core.molecules.atoms.Atom;
import core.molecules.atoms.Atom.SpecialState;
import core.molecules.organic.glycosylamines.Glycosylamine;
import core.molecules.organic.phosphate.Phosphate;

import dna.macromolecules.DNAStrand;
import dna.macromolecules.DNAStructure;
import dna.nucleobase.Nucleobase;
import dna.nucleosides.Nucleoside;
import dna.sugars.Ribose;



import resources.FactoryLibrary;
import resources.factories.AtomFactory;
import resources.factories.NucleosideFactory;
import util.XYZPosition;

/**
 * A parser built specifically to hand PDB files containing DNA molecule information
 * @author Julian
 *
 */
public class PDBparserDNA extends PDBParser<DNAStructure>{

	/**
	 * The tag associated with each ATOM line, used to differentiate atoms from other parts of PDB file
	 */
	private static final String ATOM_TAG = "ATOM"; //TODO: add more tags, increase parser abilities
	
	/**
	 * A field to hold the DNAStructure currently being worked on by this parser
	 */
	private DNAStructure DNA_STRUCT;
	
	/**
	 * reference to whatever strand is currently being added to in this structure
	 */
	private DNAStrand CURRENT_STRAND;
	/**
	 * Regex patterns used to denote different molecules within a DNA PDB as well as different states of molecules
	 */
	private static final String SUGAR_PATTERN = "['*]",
							     PHOS_PATTERN = "[pP]",
							     METHYLATED_PATTERN = "[mM]",
							     TERMINUS_PATTERN = "[tT]";
	
	@Override
	public DNAStructure parse(Scanner scan, Format format) {
		notifyObservers("Parsing Input...");
		int linenum=0;
		DNA_STRUCT = new DNAStructure(0);
		
		
		while (scan.hasNextLine()){
			System.out.println(linenum);
			linenum++;
			String line = scan.nextLine();
			this.sendUpdate(linenum);
			//Check if the current line is of proper format to be parsed
			if (!format.canApplyTo(line) || !line.startsWith(ATOM_TAG)){
				notifyObservers("Bad format @ Line "+ linenum);
				continue;
			}
			
			//parse entry on this line
			AtomEntryDNA entry = new AtomEntryDNA(line, format);
			if (DNA_STRUCT.isEmpty() || CURRENT_STRAND.isEmpty() || isOnNewNucleoside(entry)){
				if(isOnNewStrand(entry)){
					Integer strandID = entry.getStrandID();
					
					if (strandID == null && DNA_STRUCT.isEmpty()) strandID = 1;
					else if (strandID == null ) strandID = DNA_STRUCT.getHighestID()+1;
					DNA_STRUCT.add(new DNAStrand(strandID));
				}
				
				CURRENT_STRAND = findCurrentStrand(entry);
				CURRENT_STRAND.add(FactoryLibrary.getFactoryByClass(NucleosideFactory.class).newMoleculeByAbbr(entry.getNucType(), entry.getNucIndex()));
			}
			addAtomToStructure(createAtom(entry));
		}
		//remove all inconsequential components added to this strand
		DNA_STRUCT.trimEmptyMolecules();	
		return DNA_STRUCT;
		
	}


	private DNAStrand findCurrentStrand(AtomEntryDNA entry) {
		if (entry.getStrandID() == null)
			return DNA_STRUCT.last();
		return DNA_STRUCT.getSubmoleculeByID(entry.getStrandID());
	}


	/**
	 * Adds and an atom to the current nucleoside based on the Atom's tag/code - ex. 2H1
	 * @param atom
	 */
	private void addAtomToStructure(Atom atom) {
		if (isOnComponent(SUGAR_PATTERN, atom.getAtomCode())) getCurrentNucleoside().getSugar().add(atom);
		else if(isOnComponent(PHOS_PATTERN, atom.getAtomCode())) getCurrentNucleoside().getPhosphate().add(atom);
		else getCurrentNucleoside().getAmine().add(atom);
		
	}
		
	/**
	 * Retrieves the latest nucleotide added to the DNA structure
	 * @return
	 */
	private Nucleoside getCurrentNucleoside() {
		return CURRENT_STRAND.last();

	}

	/**
	 * using the regex pattern check to see if the last index of this tag confirms it as part of
	 * a given subcomponent
	 * @param regex
	 * @param atomTag
	 * @return
	 */
	private boolean isOnComponent(String regex, String atomTag) {
		
		return Pattern.matches(regex, atomTag.substring(atomTag.length()-1));
	}
	

	/**
	 * Create and atom based on the entry information. uses the Atom library to obtain the known qualities of the
	 * desired atom type
	 * @param entry
	 * @return
	 */
	private Atom createAtom(AtomEntryDNA entry) {
		Atom a = FactoryLibrary.getFactoryByClass(AtomFactory.class).newMoleculeByAbbr(entry.getAtomTypeAbbr(), 
				  new XYZPosition(entry.getX(), entry.getY(), entry.getZ()),
				  entry.getAtomTag(),
				  entry.getAtomIndex());
		
		//Apply special states //TODO: IMPROVE THIS - IF/ELSE TREE = BAD
		if (Pattern.matches("O3" + SUGAR_PATTERN, a.getAtomCode())) a.addState(SpecialState.THREE_PRIME);
		else if(Pattern.matches("O5" + SUGAR_PATTERN, a.getAtomCode())) a.addState(SpecialState.FIVE_PRIME);
		else if (Pattern.matches("C\\d+" + METHYLATED_PATTERN, a.getAtomCode())) a.addState(SpecialState.METHYLATED);
		else if (Pattern.matches("H\\d+" + TERMINUS_PATTERN, a.getAtomCode())) a.addState(SpecialState.TERMINAL);
		return a;
	}

	/**
	 * Check if the entry is on a new strand or still on the same strand being constructed
	 * @param entry
	 * @return
	 */
	private boolean isOnNewStrand(AtomEntryDNA entry) {
		if (DNA_STRUCT.isEmpty())
			return true;
		else if(CURRENT_STRAND.isEmpty()) //if current is empty, this is obviously not on a new strand
			return false;
		else if (entry.getStrandID() != null) //if you can use the StrandID stored in the entry
		{
			return !DNA_STRUCT.containsSubmoleculeByID(entry.getStrandID());
		}
		else //use the distance limit determiner
		return !isOnStrand(entry, CURRENT_STRAND);
	}
	
	/**
	 * The arbitray distance limit for determining if an atom is close enough to a given molecule to be bound to it or not
	 */
	private static final double DISTANCE_LIMIT = 6;
	
	/**
	 * Uses an arbitrary distance limit to decide if an atom is on a new Strand (beyond that arbitrary distance from every
	 * atom on the current strand).
	 * @param entry
	 * @param last
	 * @return
	 */
	private boolean isOnStrand(AtomEntryDNA entry, DNAStrand last) {
		for (Nucleoside nuc: last){
			for (Atom a: nuc.getAllSubAtoms()){
				if (a.getPosition().distance(new XYZPosition(entry.getX(), entry.getY(), entry.getZ())) < DISTANCE_LIMIT){
					
					return true;
				}
			}
		}
		return false;
	}

	
	private boolean isOnNewNucleoside(AtomEntryDNA entry) {
		
		return entry.getNucIndex() != getCurrentNucleoside().getID();//last atom added to strand
	}


	public void sendUpdate(int linenum) {
		double percent = linenum*100.0/totalLines;
		if(percent % 5 <= 10/totalLines){
			notifyObservers(Integer.toString((int)percent)+"% complete");
		}
		
	}

	

	
	
	
}
